//
//  PeerConnectionsController.m
//  SolarSeek
//
//  Created by Erick Fejta on 05/02/10.
//  Copyright 2005 Erick Fejta. All rights reserved.
//

#import "PeerConnectionsController.h"
//#import "SelfFillingMutables.h" // Pal: unused yet
#import "Connections.h"
#import "ConnectionsIdentifier.h"
#import "TimelyInformation.h"
#import "PeersController.h"
#import "Message.h"
#import "Peers.h"
#import "EnumTypes.h"
#import "SolarSeekEngine.h"
#import "ServerConnectionController.h"

@interface PeerConnectionsController (Private)
- (void)beginPeerConnectionProcess:(PeerConnection *)peerConnection;
- (void)connectToPeer:(Peer *)peer;
- (void)endPeerConnection:(PeerConnection *)peerConnection;
- (void)endRequestPeerToConnectWithToken:(NSNumber *)tokenNumber;
- (void)dispatchMessageFromServer;
- (void)dispatchMessage:(Message *)m fromPeerConnection:(PeerConnection *)pc;
- (void)openPeerConnectionIfNecessary:(PeerConnection *)peerConnection;
- (void)requestAddressForPeerConnection:(PeerConnection *)peerConnection;
- (void)requestPeerToConnect:(Peer *)peer ;
- (void)setAddress:(NSString *)address port:(UInt16)port forPeerConnection:(PeerConnection *)peerConnection;
- (void)stopObservingPeerConnection:(PeerConnection *)peerConnection;
- (void)startObservingPeerConnection:(PeerConnection *)peerConnection;
- (TimelyInformation *)timelyInformationForPeerConnection:(PeerConnection *)peerConnection address:(NSString *)address port:(UInt16)port;
- (void)updatePeer:(Peer *)peer withFileSearchResult:(Message *)message;
- (void)updatePeer:(Peer *)peer withUserInfo:(Message *)message;

- (void)addRemovablePeerConnection:(PeerConnection *)peerConnection;
@end


#pragma mark -


@interface ControlledPeerConnection : PeerConnection {
	PeerConnectionsController *_peerConnectionsController;
}

@end

@implementation ControlledPeerConnection
- (id)initWithController:(PeerConnectionsController *)peerConnectionsController peer:(Peer *)peer {
	if(self = [super initWithPeer:peer]) {
		_peerConnectionsController = peerConnectionsController;
	}
	return self;
}

- (void)dealloc {
//NSLog(@"DEALLOC: deallocating controlled for %@",[self peer]);
	[super dealloc];
}

- (void)release {
	if([self retainCount]==2) {
		[super release];
		[_peerConnectionsController addRemovablePeerConnection:self];
	}
	else
		[super release];
}

- (void)disconnect {
//NSLog(@"DISCONNECT: %@ disconnected.",[self peer]);
	[super disconnect];
}
@end

#pragma mark -


@implementation PeerConnectionsController

- (id)init {
	[NSException raise:@"InvalidArgumentException" format:@"[PeersController init] is not a valid initializer."];
	[self release];
	return nil;
}


- (id)initWithEngine:(SolarSeekEngine *)engine {
	if(self = [super init]) {
		_engine = engine;
		_connectionsIdentifier = [[engine connectionsIdentifier] retain];
			[_connectionsIdentifier addObserver:self forKeyPath:@"connectionOpened" options:nil context:nil];
		_serverConnection = [[engine serverConnection] retain];
			[_serverConnection addObserver:self forKeyPath:@"messageReceived" options:nil context:nil];
		_peersController = [[engine peersController] retain];
		_peerConnectionWithNewMessage = nil;
		_peerConnectionUnableToConnect = nil;
		//_peerConnections = [[RandomMutableSet alloc] init]; // Pal - I don't see any purpose of this
		_peerConnections = [[NSMutableSet alloc] init];
		_peerConnectionsConnecting = [[NSMutableSet alloc] init];
		_peerConnectionsConnected = [[NSMutableSet alloc] init];
		_addresses = [[NSMutableDictionary alloc] init];
		_tokensPeerConnectRequest = [[NSMutableDictionary alloc] init];
	}
	return self;
}

- (void)dealloc {
		[_connectionsIdentifier removeObserver:self forKeyPath:@"connectionOpened"];
	[_connectionsIdentifier release];
		[_serverConnection removeObserver:self forKeyPath:@"messageReceived"];
	[_serverConnection release];
	[_peersController release];
	[_peerConnections release];
	[_peerConnectionsConnected release];
	[_peerConnectionsConnecting release];
	[_peerConnectionWithNewMessage release];
	[_addresses release];
	[_tokensPeerConnectRequest release];
	[super dealloc];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
	if([object isKindOfClass:[PeerConnection class]]) {
		PeerConnection *pc = object;
		if([keyPath isEqual:@"messageReceived"]) {
			[self willChangeValueForKey:@"peerConnectionWithNewMessage"];
			[_peerConnectionWithNewMessage release];
			_peerConnectionWithNewMessage = [pc retain];
			[self dispatchMessage:[_peerConnectionWithNewMessage messageReceived] fromPeerConnection:_peerConnectionWithNewMessage];
			[self didChangeValueForKey:@"peerConnectionWithNewMessage"];
			[[_addresses objectForKey:[[pc peer] name]] refresh];	//refresh validation.
			[NSObject cancelPreviousPerformRequestsWithTarget:pc selector:@selector(disconnect) object:nil];
			[pc performSelector:@selector(disconnect) withObject:nil afterDelay:60.0];
		}
		else if([keyPath isEqual:@"connected"]) {
			if([pc connected]) {
			//NSLog(@"PEER: Connected to %@.",[[pc peer] name]);
				[_peerConnectionsConnected addObject:pc];
				
				[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(endPeerConnection:) object:pc];
				[pc performSelector:@selector(disconnect) withObject:nil afterDelay:60.0];
				
				
				TimelyInformation *ti = [self timelyInformationForPeerConnection:pc address:[pc addressHost] port:[pc portHost]];
				[[_addresses objectForKey:[[pc peer] name]] removeObserver:self forKeyPath:@"info"];
				[ti addObserver:self forKeyPath:@"info" options:NSKeyValueObservingOptionOld context:nil];
				[_addresses setObject:ti forKey:[[pc peer] name]];
			}
			else {
				[self endPeerConnection:pc];								
			}
		}
		else if([keyPath isEqual:@"messageSending"] && [pc hasMessagesToSend] && [pc connected]==NO) {
			[self beginPeerConnectionProcess:pc];
		}
		else if([keyPath isEqual:@"messageSent"]) {
			[[_addresses objectForKey:[[pc peer] name]] refresh];	//refresh validation.
			[NSObject cancelPreviousPerformRequestsWithTarget:pc selector:@selector(disconnect) object:nil];
			[pc performSelector:@selector(disconnect) withObject:nil afterDelay:60.0];
		}
	}
	else if(object==_serverConnection && [keyPath isEqual:@"messageReceived"]) {
		[self dispatchMessageFromServer];
	}
	else if(object==_connectionsIdentifier && [keyPath isEqual:@"connectionOpened"]) {
		UnidentifiedConnection *uc = [_connectionsIdentifier connectionOpened];
		if([[uc type] isEqual:TypeConnectionPeer]) {
			PeerConnection *pc = [self peerConnectionForName:[uc name]];
			[_peerConnectionsConnecting addObject:pc];
			[pc changeSocket:[uc socket]];
		}
		else if([[uc type] isEqual:TypeConnectionPierceFirewall]) {
			unsigned token = [uc token];
			NSNumber *num = [NSNumber numberWithUnsignedInt:token];
			Peer *p = [_tokensPeerConnectRequest objectForKey:num];
			if(p != nil) {
				[NSObject cancelPreviousPerformRequestsWithTarget:_tokensPeerConnectRequest selector:@selector(endRequestPeerToConnectWithToken:) object:num];
				PeerConnection *pc = [self peerConnectionForPeer:p];
				[_tokensPeerConnectRequest removeObjectForKey:num];
				[pc changeSocket:[uc socket]];
			}
		}
	}
	else if([object isKindOfClass:[TimelyInformation class]] && [keyPath isEqual:@"info"] && [object info] == nil) {	//we were invalidated!
		[object removeObserver:self forKeyPath:@"info"];
		NSDictionary *d = [change objectForKey:NSKeyValueChangeOldKey];
		PeerConnection *pc = [d objectForKey:@"PeerConnection"];
	//NSLog(@"Invalidating address info for %@.",[[pc peer] name]);
		[_addresses removeObjectForKey:[[pc peer] name]];
	}
}

#pragma mark KVO

- (PeerConnection *)peerConnectionWithNewMessage { return _peerConnectionWithNewMessage; }
- (PeerConnection *)peerConnectionUnableToConnect { return _peerConnectionUnableToConnect; }

- (PeerConnection *)peerConnectionForPeer:(Peer *)peer { 
	PeerConnection *pc1 = [[PeerConnection alloc] initWithPeer:peer];
	PeerConnection *pc = [_peerConnections member:pc1]; 
	[pc1 release];
	if(pc==nil) {
		pc = [[[ControlledPeerConnection alloc] initWithController:self peer:peer] autorelease];
		[self startObservingPeerConnection:pc];
	}
	return pc;
}
- (PeerConnection *)peerConnectionForName:(NSString *)namePeer { return [self peerConnectionForPeer:[[NSApp engine] peerNamed:namePeer]]; }

#pragma mark actions (KVO)


- (void)beginPeerConnectionProcess:(PeerConnection *)peerConnection {
	NSAssert1([peerConnection connected]==NO,@"Cannot begin the connectio process for %@ because we are already conected to this user.",[peerConnection peer]);
	
	[_peerConnectionsConnecting addObject:peerConnection];
	
//NSLog(@"PEER: Beginning connection to %@",[peerConnection peer]);
	
	
	if([[_engine connectionsIdentifier] shouldRequestIncomingConnectionFirst] ) { 
		//then have peer connect here.
		[self requestPeerToConnect:[peerConnection peer]];
	}
	else {	//connect to peer directly.
		[self connectToPeer:[peerConnection peer]];
	}
}

- (void)dispatchMessageFromServer {
	Message *m = [_serverConnection messageReceived];
	[m rewind];
	int code = [m getCode];
	switch(code) {
		case GetPeerAddress: {
			NSString *name = [m getString];
			int addr = [m getInt];
			int port = [m getInt];
			[self setAddress:MakeNSStringFromInternetAddress(addr) port:port forPeerConnection:[self peerConnectionForName:name]];
			break;
		}
		case ConnectToPeer: {
			NSString *name = [m getString];
			NSString *type = [m getString];
			int addr = [m getInt];
			int port = [m getInt];
			int pass = [m getInt];
			PeerConnection *pc = [self peerConnectionForName:name];
			[self setAddress:MakeNSStringFromInternetAddress(addr) port:port forPeerConnection:pc];
			if([type isEqual:TypeConnectionPeer]) {
			//NSLog(@"PEER: Received request to connect to %@.  Connecting...",name);
				[_peerConnectionsConnecting addObject:pc];
				[self performSelector:@selector(endPeerConnection:) withObject:pc afterDelay:30.0];
				[pc pierceFirewallWithToken:pass ForHost:MakeNSStringFromInternetAddress(addr) onPort:(UInt16)port];
			}
			else if([type isEqual:TypeConnectionTransfer] == NO)
				NSLog(@"Received strange connection request from %@ of type %@.",name,type);
			break;
		}
		case CantConnectToPeer: {
			unsigned token = [m getInt];
			[self endRequestPeerToConnectWithToken:[NSNumber numberWithUnsignedInt:token]];
			break;
		}
	}
}

- (void)dispatchMessage:(Message *)message fromPeerConnection:(PeerConnection *)peerConnection {
	[message rewind];
	int code = [message getCode];
//NSLog(@"Received message with code %d from %@",code,[[peerConnection peer] name]);
	NS_DURING
		switch(code) {
		case UserInfoReply:
			[NSApp appendLogWithHeader:@"INFO" 
				HTML:[[[peerConnection peer] name] stringByAppendingString:@" sent information about him or herself."]
				type:LogTypePeer];
			[self updatePeer:[peerConnection peer] withUserInfo:message];
			break;
		case FileSearchResult:
			[self updatePeer:[peerConnection peer] withFileSearchResult:message];
			break;
		case AssertSolarSeekPrivileged: 
		case AssertSolarSeekUser: {
			[[peerConnection peer] addPrivileges:PeerPrivilegesSS];
			break;
		}
		case AnswerBuddyIcon: {
			BOOL b = [message getByte];
			if(b)
				[[[NSApp engine] peersController] refreshInformationForPeer:[peerConnection peer]];
			break;
		}
		case QuestionBuddyIcon: {
			Message *m = [Message messageWithCode:AnswerBuddyIcon];
			NSData *d = [[[NSApp engine] me] picture];
			BOOL b = d && [d length] > 0;
			[m putByte:b];
			break;
		}
		default:
			break;
		}
	NS_HANDLER
	//NSLog(@"ERROR: Could not decode %d byte message with code %d from %@: %@",[[message data] length],code,[peerConnection peer],localException);
	//NSLog(@"ERROR: Ignoring exception and continuing...");
	NS_ENDHANDLER
}

#pragma mark actions (internal)

- (void)addRemovablePeerConnection:(PeerConnection *)peerConnection {
	[self stopObservingPeerConnection:peerConnection];
}


- (void)connectToPeer:(Peer *)peer {
	NSTimeInterval seconds = 15.0;
	PeerConnection *pc = [self peerConnectionForPeer:peer];
	NSDictionary *info = [[_addresses objectForKey:[peer name]] info];
	if(info!=nil) {
		NSString *address = [info objectForKey:@"Address"];
		UInt16 port = [[info objectForKey:@"Port"] unsignedShortValue];
		if(port != 0) {
		//NSLog(@"PEER: Connecting to %@ directly at %@:%d...",peer,address,port);
			[pc connectToHost:address onPort:port withUsername:[[_engine serverConnectionController] username]];
			[self performSelector:@selector(endPeerConnection:) withObject:pc afterDelay:seconds];
		}
		else {
		//NSLog(@"Failed to connect to %@ directly because the user is listening on port 0.",peer);
			[self endPeerConnection:pc];
		}
	}
	else {
		[self requestAddressForPeerConnection:pc];
	}
}	

- (void)endPeerConnection:(PeerConnection *)peerConnection {
	[peerConnection disconnect];
	if([peerConnection hasMessagesToSend]) {
		if([peerConnection piercingFirewall]==NO && [[_engine connectionsIdentifier] shouldRequestIncomingConnectionSecond]) {
			[self requestPeerToConnect:[peerConnection peer]];
		}
		else {
		//NSLog(@"PEER: Unable to open a connection to %@",[peerConnection peer]);
			[self setAddress:nil port:0 forPeerConnection:peerConnection];
			if([peerConnection piercingFirewall]==YES) {
				Message *m = [Message messageWithCode:CantConnectToPeer];
				[m putInt:[peerConnection tokenFirewall]];
				[[_engine serverConnection] sendMessage:m];
			}
			[peerConnection removeMessagesUnsent];			
			[self willChangeValueForKey:@"peerConnectionUnableToConnect"];
			[_peerConnectionUnableToConnect release];
			_peerConnectionUnableToConnect = [peerConnection retain];
			[self didChangeValueForKey:@"peerConnectionUnableToConnect"];
				
		}
	}
	else {
	//NSLog(@"PEER: Connection with %@ ended naturally.",[peerConnection peer]);
	}
	[_peerConnectionsConnecting removeObject:peerConnection];
	[_peerConnectionsConnected removeObject:peerConnection];
}

- (void)endRequestPeerToConnectWithToken:(NSNumber *)tokenNumber {
	Peer *p = [_tokensPeerConnectRequest objectForKey:tokenNumber];
	if(p==nil) return;
//NSLog(@"PEER: Requesting %@ to connect failed (%d).",p,[tokenNumber unsignedIntValue]);
	[_tokensPeerConnectRequest removeObjectForKey:tokenNumber];
	if([[_engine connectionsIdentifier] shouldRequestIncomingConnectionFirst]) {
		[self connectToPeer:p];
	}
	else {
		PeerConnection *pc = [self peerConnectionForPeer:p];
		[pc removeMessagesUnsent];
		[self willChangeValueForKey:@"peerConnectionUnableToConnect"];
		[_peerConnectionUnableToConnect release];
		_peerConnectionUnableToConnect = [pc retain];
		[self didChangeValueForKey:@"peerConnectionUnableToConnect"];
		[_peerConnectionsConnecting removeObject:pc];
		[_peerConnectionsConnected removeObject:pc];
	}
}


- (void)requestAddressForPeerConnection:(PeerConnection *)peerConnection {
//NSLog(@"PEER: Asking for %@'s address.",[peerConnection peer]);
	Message *m = [Message message];
	[m putCode:GetPeerAddress];
	[m putString:[[peerConnection peer] name]];
	[_serverConnection sendMessage:m];
	[self performSelector:@selector(endPeerConnection:) withObject:peerConnection afterDelay:15.0];
}



- (void)requestPeerToConnect:(Peer *)peer {
	Message *m = [Message messageWithCode:ConnectToPeer];
	unsigned token = [_serverConnection tokenNext];
	[m putInt:token];
	[m putString:[peer name]];
	[m putString:TypeConnectionPeer];
	NSNumber *num = [NSNumber numberWithUnsignedInt:token];
//NSLog(@"PEER: Asking %@ to connect with on token %d.",[peer name],token);
	[_tokensPeerConnectRequest setObject:peer forKey:num];
	[self performSelector:@selector(endRequestPeerToConnectWithToken:) withObject:num afterDelay:60.0];
	[_serverConnection sendMessage:m];
}

//fejta: TODO: idea: some sort of struct that holds the message and returns info about stuff?

- (void)setAddress:(NSString *)address port:(UInt16)port forPeerConnection:(PeerConnection *)peerConnection {
//NSLog(@"PEER: Updating address and port for %@ to %@:%d.",[peerConnection peer],address,port);
	if(address==nil || port==0) {
		TimelyInformation *ti = [_addresses objectForKey:[[peerConnection peer] name]];
		[ti invalidate];
		[_addresses removeObjectForKey:[[peerConnection peer] name]];
	}
	else {
		TimelyInformation *ti = [self timelyInformationForPeerConnection:peerConnection address:address port:port];
		[ti addObserver:self forKeyPath:@"info" options:NSKeyValueObservingOptionOld context:nil] ;
		[[_addresses objectForKey:[[peerConnection peer] name]] removeObserver:self forKeyPath:@"info"];
		[_addresses setObject:ti forKey:[[peerConnection peer] name]];
		SolarSeekAddress *addy = [[SolarSeekAddress alloc] initWithAddress:address port:port];
		[[peerConnection peer] setAddress:addy];
		[addy release];
		[self openPeerConnectionIfNecessary:peerConnection];
	}
}

- (void)startObservingPeerConnection:(PeerConnection *)peerConnection {
//NSLog(@"PC: Observing %@(%d).",[peerConnection peer],peerConnection);
	[_peerConnections addObject:peerConnection];
	[peerConnection addObserver:self forKeyPath:@"connected" options:nil context:nil];
	[peerConnection addObserver:self forKeyPath:@"messageSending" options:nil context:nil];
	[peerConnection addObserver:self forKeyPath:@"messageSent" options:nil context:nil];
	[peerConnection addObserver:self forKeyPath:@"messageReceived" options:nil context:nil];
}

- (void)stopObservingPeerConnection:(PeerConnection *)peerConnection {
//NSLog(@"PC: No longer observing %@(%d).",[peerConnection peer],peerConnection);
	[peerConnection removeObserver:self forKeyPath:@"connected"];
	[peerConnection removeObserver:self forKeyPath:@"messageSending"];
	[peerConnection removeObserver:self forKeyPath:@"messageSent"];
	[peerConnection removeObserver:self forKeyPath:@"messageReceived"];
	[_peerConnections removeObject:peerConnection];
}


- (TimelyInformation *)timelyInformationForPeerConnection:(PeerConnection *)peerConnection address:(NSString *)address port:(UInt16)port {
	NSDictionary *d = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:address,[NSNumber numberWithUnsignedInt:port],peerConnection,nil] forKeys:[NSArray arrayWithObjects:@"Address",@"Port",@"PeerConnection",nil]];
	float time = 60.0*60.0;		//1 hour
	return [TimelyInformation timelyInformation:d validForSeconds:time];
}



- (void)openPeerConnectionIfNecessary:(PeerConnection *)peerConnection {
	//If we need to, we connect depending on the firewall status.
	
	//(We don't know the status)
	//	shouldRequestIncomingConnectionFirst will be no, so open the connection directly.
	//  we will then observe a change in the connected key.
	//	if we disconnect without sending the header then we will try having them connect to us.
	//	we send the request message and wait for the unidentified connection to be opened.

	if([peerConnection hasMessagesToSend] && [peerConnection connected]==NO) {
		[self beginPeerConnectionProcess:peerConnection];
	}
}


- (void)updatePeer:(Peer *)peer withFileSearchResult:(Message *)message {
	if([message decompress]) {
		[message getString]; //name
		[message getInt]; //token
		int files = [message getInt];
		while(files--) {
			[message getByte];
			[message getData];
			[message getLongLong];
			[message getString];
			int attributes = [message getInt];
			while(attributes--) {
				[message getInt];
				[message getInt];
			}
		}
		NSNumber *countSlotsFree = [NSNumber numberWithUnsignedChar:[message getByte]];
		NSNumber *speedUpload = [NSNumber numberWithUnsignedInt:[message getInt]];
		NSNumber *countQueued = [NSNumber numberWithUnsignedInt:[message getInt]];
		
		NSDictionary *d = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:countSlotsFree,speedUpload,countQueued,nil]
				forKeys:[NSArray arrayWithObjects:@"countSlotsAvailable",@"speedAverage",@"countDownloadsQueued",nil]];
		[peer setValuesForKeysWithDictionary:d];
	}
//	else
	//NSLog(@"Could not decompress search results from %@.",peer);
}

- (void)updatePeer:(Peer *)peer withUserInfo:(Message *)message {
	NSString *desc = [message getString];
	id pic = nil;
	if([message getByte] != 0) {
		NSData *d = [message getData];
		pic = [[NSImage alloc] initWithData:d];
		[pic release];
		if(pic) {
			pic = [d retain];
		}
	}
	else
		pic = [NSNull null];
	unsigned slotstotal = [message getInt],
			 queue = [message getInt],
			 slotsavailable = [message getByte];
	NSDictionary *dic = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:desc,pic,
		[NSNumber numberWithUnsignedInt:slotsavailable],[NSNumber numberWithUnsignedInt:slotstotal],
		[NSNumber numberWithUnsignedInt:queue],nil] 
		forKeys:[NSArray arrayWithObjects:@"comment",@"picture",@"countSlotsAvailable",@"countSlotsTotal",@"countDownloadsQueued",nil]];
	[peer setValuesForKeysWithDictionary:dic];
}
@end
